docs(guide): repair the seven ${…} sites on keys with no carriage row (objectui#7872) - #7900
Merged
os-sam merged 1 commit intoSep 6, 2026
Conversation
… (objectui#7872)
`SchemaRenderer` evaluates `content`, the `properties`/`props` bags, the eight
condition keys, and the carriage rows `expressionBindableTextKeysFor` gives the
node's own type. `badge`, `alert` and `page` have no rows, so every `${…}` these
three pages wrote on `badge.text`, `badge.variant`, `alert.variant`,
`alert.message` and `page.title` reached the renderer as the characters the
author typed.
Repaired per passage under the 2026-09-01 fork-B ruling on objectui#7115, not by
one blanket rule: the passages demonstrating binding keep demonstrating it on a
key that genuinely carries (`card.title`/`description`, `statistic.label`/
`value`/`description`, a nested `text` node's `content`), and the page-layout
passage takes a literal plus prose naming the missing row.
Two of the old shapes were broken beyond not evaluating, re-measured here
through the real renderers rather than carried over from objectui#7847:
`text` is not a `BadgeSchema` key (the badge's text is `label`, so the old shape
rendered an empty badge with `text=${…}` left on the DOM), and `success` /
`warning` / `info` are not badge variants. Newly measured on this card:
`message` is not an `AlertSchema` key either — the alert's text keys are `title`
and `description` — and `AlertSchema.variant` is the closed set `default` |
`destructive`, so `info` / `warning` / `success` / `error` are all off-enum.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
os-sam
marked this pull request as ready for review
September 6, 2026 02:19
os-sam
deleted the
claude/issue-7872-uncarried-expression-sites-three-pages
branch
September 6, 2026 02:35
This was referenced Sep 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #7872
Seven
${…}sites in three guide pages were written on keys with no carriage row, so the renderer received the characters the author typed. Repaired per passage under the 2026-09-01 fork-B ruling on #7115, never one blanket rule.Measurement 1 — is #7868's pin corpus-count-sensitive? NO, so
scripts/is untouchedscripts/__tests__/check-doc-expression-carriage.test.tsasserts against fixtures, and its one live-corpus case says in a comment why it deliberately does not pin the count:Its vacuity guards are
fences > 100andnodes > 100(204 and 361 here), andCONTROL_FIXTURESare inline string literals in the gate itself, not read fromcontent/docs. Confirmed by running, not only by reading: all 28 cases pass after the corpus count moved, so the file surface is the three doc pages alone.The card's headline of 17 was measured on
59df1dab2, before PR #7847 landed. On this branch's base655afabf2the census already read 11 — #7847 repaired 6 ofguide/expressions.md's 8 sites in the meantime. Removing this card's 7 therefore lands on 4, and those 4 are exactly the remainder the dispatch enumerated:guide/expressions.mdlist.items×2 — finding(docs):guide/expressions.mdstill authors${…}in 6 more keys with no carriage row —badge.text/badge.variant/card.className/card.body, beyond the two the 2026-09-01 ruling named #7418, deliberately unclaimedschema-rendering.md:145chart.data— finding(docs/guide): schema-rendering.md teaches a chart node binding node-leveldatato a${...}expression — not evaluated (the #6665 shape), and a parse refusal since #7113 #7680api/schema-reference.md:612action.condition— considered and deliberately not claimedSo the attribution is right and the corpus is intact; only the starting number was stale. Nothing was adjusted to make the arithmetic come out — no number is pinned anywhere in the repo (
git grep uncarriedfinds only the gate's own prose).The complete new census line, on
74f19969a:Measurement 5 — the carriage map, re-read from the artifact
Read out of the built
@objectstack/spec@17.2.0dist/ui/index.mjs, the lookupSchemaRenderer.tsx:1139consumes, rather than copied from the card's table:Three types carry anything at all.
expressionBindableTextKeysFor('badge'),('alert')and('page')each return the frozen empty array — matching the card. Channels, derived fromSchemaRenderer's own call sites:content· bagsproperties,props· conditionsdisabled,disabledOn,hidden,hiddenOn,visibility,visible,visibleOn,visibleWhen.Measurement 3 — both claims re-verified on current main, and one extended
Not taken on trust from #7847:
textis not aBadgeSchemakey.packages/types/src/data-display.ts:93and the zod mirrorzod/data-display.zod.ts:54both declarelabel/variant/icon/children;renderers/data-display/badge.tsxrendersschema.label || renderChildren(schema.body). Rendered through the real renderer,{ type: 'badge', text: '${status}' }produces an empty badge withtext="${status}"left on the element —SchemaRendererspreads the node's post-strip top level as React props, which is whatcollectUnevaluatedExpressionsscans for the "Unevaluated expression reached the DOM" diagnostic.success/warning/infoare not badge variants. Closed setdefault|secondary|destructive|outline, agreeing in three places: the TS union, the zod enum, and the renderer's own registrationinputs.Newly measured here, which the card left open ("Not asserted for
alert"):messageis not anAlertSchemakey either.AlertSchemadeclarestitle/description/variant/icon/dismissible/children;renderers/data-display/alert.tsxrendersschema.titleandschema.description || renderChildren(schema.body). Soalert.messagewas the same defect asbadge.text, not merely an unevaluated expression.AlertSchema.variantis the closed setdefault|destructive— only two members. The oldComplex Logicsite computedinfo/warning/successand the oldError Statessite authored the literalerror; all four are off-enum.The seven positions, located by content on
655afabf2guide/schema-rendering.mdbadge.textguide/schema-rendering.mdbadge.variantguide/schema-rendering.mdalert.variantguide/schema-rendering.mdalert.messageguide/architecture.mdbadge.textguide/architecture.mdbadge.variantguide/layout.mdpage.titleSix of the seven sit under headings that are teaching binding — "Conditional Expressions", "Complex Logic", "Error States", "Data Transformations" — so de-expressing them would have deleted the lesson. They move onto keys that genuinely carry:
card.title/card.description,statistic.label/value/description, and a nestedtextnode'scontent, whichSchemaRendererevaluates on every node type (if (typeof newSchema.content === 'string'), no type gate). The nested form is the honest escape hatch for a non-carrying component:alert's renderer falls back fromdescriptiontorenderChildren(schema.body), andrenderChildrenroutes the child throughSchemaRenderer, so the innercontentis evaluated. Each repaired fence also carries prose naming the missing row and the working alternative, matching how #7115 rewrote progress and input.layout.md's site is the one incidental case — the passage is about page structure with actions, not about binding — so its title becomes a literal with prose pointing at the host and at acardin the body.Bounded in-place fix, declared
layout.md's same fence spelled${record.name}a second time, in a breadcrumb item'slabel. The census cannot see it (a breadcrumb item is not a typed node) and it is not one of the seven. It is made literal anyway, because the prose this PR adds four lines above says to resolverecord.namein the host — leaving the expression in place would have contradicted the sentence being added. Whether breadcrumb labels are evaluated was not measured; a literal is correct either way, which is why this repair does not depend on the answer.The docs' bare root spelling is host scope, not a defect — measured before deciding
Rendering the repaired fences with only a
dataSourcemounted, nothing resolved:${status}came back verbatim while${data.status}gave"active". That was the harness, not the pages.SchemaRendererbuilds its evaluator as{ ...predicateScope, current_user, [record], data: dataSource, page }, andPredicateScopeProvidertakes an arbitraryRecordof top-level names that app-shell'sExpressionProviderfeeds. So bare${status}/${orders.length}resolve when the host binds them — and it is the convention #7847 kept throughoutexpressions.md(${user.firstName},${score >= 90 ? …}). The spelling is therefore left alone; changing it would have been an unmeasured rewrite.Verification
Every repaired fence is parsed out of the shipped page and rendered through the real production renderers with
PredicateScopeProvider+SchemaRendererProvidermounted — 8 cases, all green: the card rendersActive/This record is in use., the Complex Logic alert rendersWelcome!/You are all caught up., the Error States alert rendersUpstream timed out, the statistic renders12/Above target, no${survives in any DOM, andlayout.md's fence holds no${…}at all. Three negative cases pin the old shapes: an empty badge with${status}on the DOM,badge.labelrendering correctly, andalert.messageleaving the description slot empty.Ablation — with the pre-repair pages restored onto disk from
655afabf2(mutation confirmed on disk by grep count, 1/1/1 present and the repairedstatisticfence 0), the same harness went 5 failed | 3 passed: the five repaired-site cases fail and the three old-shape cases pass, which is the instrument seeing the difference rather than a check that cannot fail. The restore leg was proven byte-identical — each file'sgit hash-objectequals itsHEADblob (61257c6cb,35c560cf5,f54c7cd88) andgit diff HEADis empty — and the harness returned to 8/8 afterwards. The harness itself is a throwaway and is not part of this PR;git statusis clean.Gate union re-run after the final commit, on
74f19969a, withgit diff HEADempty. Exit codes captured by redirecting first, never through a pipe.check-doc-expression-carriage✅ Controls pass: this gate can see the class it is looking for, and does not cry wolf.Test Files 1 passed (1)·Tests 28 passed (28)check:doc-types✅ Every documented component type is registered.check:doc-fences✅ check:doc-fences — every TypeScript block in 227 document(s) is fenced ts/tsx/typescript …check:doc-snippetsSemantic phase: 456 of 456 block(s) judged, 0 failed.check:doc-example-readersOK 80 documented symbol(s), 3945 call site(s) … no @example hand-spells one.check:docs-route-closurecheck-changeset-presence✅ No source or published contract of a released package changed in this range, so no changeset is owed.✅ NOT GOVERNED — 3 path(s) checked against 5 governed surface(s); none matched.check:doc-snippetsfirst answered EXIT=2 (precondition not met — packages unbuilt). Its scoped closure was built withpnpm exec turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter) --concurrency=2(34 tasks, all successful) and it was re-run for the real green above.Control-byte self-scan of the three edited files:
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'matched nothing.Not run locally, declared: the repo-wide
pnpm lintand the rest of the CI farm. CI runs the farm exactly once regardless; the gates above are the family that reads the files this diff changes.Out of scope, filed
#7898 — measured in these same pages while working here, and not repaired: six
buttonnodes authortext(not aButtonSchemakey; the renderer readslabel) and three fences author a top-levelonClick(handlerKeyRefusal(..., 'runtime-slot'), refused by name). Both hold literals, so the carriage census cannot see them andcheck:doc-typesjudges only thetypeliteral.quick-start.md:149'sonClickis recorded there as measured-and-NOT-a-defect: it sits inside theeventsbag, which is the correct dialect.Untouched, as instructed:
guide/expressions.md,schema-rendering.md'schart.datasite,api/schema-reference.md, andscripts/check-doc-expression-carriage.mjs's predicate and scan population.🤖 Generated with Claude Code
Generated by Claude Code